﻿using System.Globalization;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Devonline.AspNetCore;

/// <summary>
/// 附件及文件操作类服务
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
/// <typeparam name="TAttachment">附件数据类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public class AttachmentService<TDbContext, TAttachment, TKey>(
    ILogger<AttachmentService<TDbContext, TAttachment, TKey>> logger,
    TDbContext context,
    IDistributedCache cache,
    IHttpContextAccessor httpContextAccessor,
    IFileService fileService,
    IImageService imageService,
    IServiceProvider serviceProvider,
    HttpSetting httpSetting) :
    DataService<TDbContext, TAttachment, TKey>(logger, context, cache, httpContextAccessor, httpSetting),
    IAttachmentService<TDbContext, TAttachment, TKey>,
    IDataService<TDbContext, TAttachment, TKey>
    where TDbContext : DbContext
    where TAttachment : class, IAttachment<TKey>, new()
    where TKey : IConvertible
{
    protected readonly IFileService _fileService = fileService;
    protected readonly IImageService _imageService = imageService;
    protected readonly IServiceProvider _serviceProvider = serviceProvider;
    /// <summary>
    /// 允许上传的文件类型
    /// </summary>
    protected string[] AllowFileExtensions => (_httpSetting.Attachment?.Upload.Extensions ?? DEFAULT_ALLOW_FILE_EXTENSIONS).Split(CHAR_COMMA, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

    /// <summary>
    /// 获取并返回文件名指向的物理文件
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <returns></returns>
    public virtual async Task<string?> GetFileAsync(string fileName) => await _fileService.GetFileAsync(fileName);
    /// <summary>
    /// 获取并返回文件名指向的图片物理文件
    /// </summary>
    /// <param name="fileName">图片文件名</param>
    /// <returns></returns>
    public virtual async Task<string?> GetImageFileAsync(string fileName)
    {
        var filePath = await _fileService.GetFileAsync(fileName);
        if (File.Exists(filePath))
        {
            //如果有图片缩放参数, 则进行图片缩放处理
            return GetImageFile(filePath);
        }

        return null;
    }
    /// <summary>
    /// 获取图片文件中的嵌入信息
    /// </summary>
    /// <param name="fileName">图片文件名</param>
    /// <returns></returns>
    public virtual async Task<string?> GetImageInformationAsync(string fileName)
    {
        var filePath = await _fileService.GetFileAsync(fileName);
        if (File.Exists(filePath))
        {
            //如果图片存在, 则获取图片信息
            return _imageService.GetInformation(filePath);
        }

        return null;
    }

    /// <summary>
    /// 获取附件对应的文件, 适用于有附件记录的文件下载
    /// </summary>
    /// <param name="fileName"></param>
    /// <param name="filePath"></param>
    /// <returns></returns>
    /// <exception cref="BadHttpRequestException"></exception>
    public virtual async Task<string?> GetAttachmentFileAsync(string? fileName, string? filePath = default)
    {
        var attachment = await GetAttachmentAsync(fileName, filePath);
        if (attachment != null && !string.IsNullOrWhiteSpace(attachment.Path))
        {
            return await _fileService.GetFileAsync(attachment.Path);
        }

        return null;
    }

    /// <summary>
    /// 文件下载, 提供类似于文件夹目录结构的访问方式进行下载
    /// 文件路径只支持一级路径, 适用于访问自动保存的文件或者符合目录规范的文件
    /// 形如 2020-01-01/xxx.jpg
    /// </summary>
    /// <param name="fileName">文件名</param>
    /// <param name="filePath">文件路径(只支持一级路径)</param>
    /// <returns></returns>
    public virtual async Task<TAttachment?> GetAttachmentAsync(string? fileName, string? filePath = default)
    {
        fileName ??= _request.GetRequestOption<string>(nameof(fileName));
        filePath ??= _request.GetRequestOption<string>(nameof(filePath));
        if (string.IsNullOrWhiteSpace(fileName))
        {
            _logger.LogError($"{_userAccess}: 缺少必须的文件名!");
            throw new ArgumentNullException(fileName, "缺少必须的文件名!");
        }

        _logger.LogInformation($"{_userAccess}: get the file from path <{filePath}> and name <{fileName}>");
        filePath = string.IsNullOrWhiteSpace(filePath) ? fileName : Path.Combine(filePath, fileName);
        return await _dbSet.FirstOrDefaultAsync(x => x.Path == filePath).ConfigureAwait(false);
    }
    /// <summary>
    /// 获取文件, 按业务类型和业务主键获取
    /// </summary>
    /// <param name="businessKey">业务主键</param>
    /// <param name="businessType">业务类型</param>
    /// <returns></returns>
    public virtual async Task<IEnumerable<TAttachment>?> GetAttachmentsAsync(TKey businessKey, string? businessType = default)
    {
        businessType ??= _request.GetRequestOption<string>(nameof(businessType));
        _logger.LogInformation($"{_userAccess}: query the attachment list with businessType {businessType} and businessKey {businessKey}");

        var queryable = GetQueryable(x => x.BusinessKey != null && x.BusinessKey.Equals(businessKey));
        if (!string.IsNullOrWhiteSpace(businessType))
        {
            queryable = queryable.Where(x => x.BusinessType == businessType);
        }

        var attachments = await queryable.ToListAsync();
        _logger.LogInformation($"{_userAccess}: query the attachment list with businessType {businessType} and businessKey {businessKey} success");
        return attachments;
    }

    /// <summary>
    /// 单个文件上传
    /// </summary>
    /// <typeparam name="TUploadResult">上传结果类型</typeparam>
    /// <param name="file">上传的文件</param>
    /// <returns>上传结果</returns>
    public virtual async Task<TUploadResult> UploadAsync<TUploadResult>(IFormFile? file = default) where TUploadResult : UploadResult<TAttachment, TKey>, new()
    {
        if (file is null)
        {
            var form = await _request.ReadFormAsync();
            if (_request.HasFormContentType && form.Files.Any())
            {
                file = form.Files[0];
            }
        }

        if (file is null)
        {
            throw new BadHttpRequestException("没有提交任何文件!");
        }

        var fileName = string.IsNullOrWhiteSpace(file.FileName) ? file.Name : file.FileName;
        var uploadResult = new TUploadResult
        {
            FileName = fileName,
            StatusCode = HttpStatusCode.NotAcceptable
        };

        if (file.Length <= 0)
        {
            uploadResult.Result = "文件内容为空!";
            return uploadResult;
        }

        //根据contentType 获取配置文件中文件大小
        var index = file.ContentType.IndexOf(CHAR_SLASH);
        index = index > UNIT_ZERO && index < file.ContentType.Length ? index : file.ContentType.Length;
        var fileSize = file.ContentType[..index] switch
        {
            "image" => _httpSetting.Attachment.Upload.Length,
            "video" => _httpSetting.Attachment.Video.Length,
            _ => _httpSetting.Attachment.Upload.Length
        };

        if (file.Length > fileSize)
        {
            uploadResult.Result = $"文件 {file.Name} 大小超出 {fileSize} 单文件最大上传限制!";
            return uploadResult;
        }

        var ext = Path.GetExtension(fileName);
        var extUpper = ext.ToUpperInvariant();
        if (!AllowFileExtensions.Any(x => x == extUpper))
        {
            uploadResult.Result = "文件类型禁止上传!";
            return uploadResult;
        }

        var attachment = new TAttachment()
        {
            Name = fileName,
            Length = file.Length,
            Extension = ext,
            ContentType = file.ContentType
        };

        var attachmentPath = DateTime.UtcNow.ToString(DEFAULT_DATE_FORMAT, CultureInfo.InvariantCulture);
        var attachmentFile = _fileService.GetAttachmentPath(attachmentPath);
        if (!Directory.Exists(attachmentFile))
        {
            Directory.CreateDirectory(attachmentFile);
        }

        fileName = KeyGenerator.GetKey() + ext;
        attachment.Path = Path.Combine(attachmentPath, fileName);
        fileName = Path.Combine(attachmentFile, fileName);

        try
        {
            using (var stream = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
            {
                await file.CopyToAsync(stream).ConfigureAwait(false);
                stream.Close();
            }

            uploadResult.StatusCode = HttpStatusCode.OK;
            uploadResult.Result = attachment.Path;
            uploadResult.Attachment = attachment;

            _logger.LogInformation($"{_userAccess}: upload the file <{uploadResult.FileName}> success with result <{uploadResult.Result}>!");

            //附加操作
            await AttachAsync(attachment.ContentType, fileName);
        }
        catch (Exception ex)
        {
            uploadResult.StatusCode = HttpStatusCode.NotAcceptable;
            uploadResult.Result = ex.Message;
            _logger.LogError(ex, $"{_userAccess}: upload the file <{uploadResult.FileName}> fail with error: <{ex.GetMessage()}>!");
        }

        return uploadResult;
    }
    /// <summary>
    /// 批量文件上传
    /// </summary>
    /// <typeparam name="TUploadResult">上传结果类型</typeparam>
    /// <param name="files">上传的文件</param>
    /// <returns>上传结果</returns>
    public virtual async Task<IEnumerable<TUploadResult>> UploadAsync<TUploadResult>(IEnumerable<IFormFile>? files = default) where TUploadResult : UploadResult<TAttachment, TKey>, new()
    {
        if (files == null || (!files.Any()))
        {
            var form = await _request.ReadFormAsync();
            if (_request.HasFormContentType && form.Files.Any())
            {
                files = form.Files;
            }
        }

        if (files == null || (!files.Any()) || files.Sum(x => x.Length) <= 0)
        {
            throw new BadHttpRequestException("没有提交任何文件!");
        }

        var fileSize = _httpSetting.Attachment?.Upload.Total ?? DEFAULT_TOTAL_FILE_SIZE;
        if (files.Sum(x => x.Length) > fileSize)
        {
            throw new BadHttpRequestException($"文件总大小超出 {fileSize} 最大上传限制!");
        }

        var filePath = DateTime.UtcNow.ToString(DEFAULT_DATE_FORMAT, CultureInfo.InvariantCulture);
        var attachmentPath = _fileService.GetAttachmentPath(filePath);
        if (!Directory.Exists(attachmentPath))
        {
            Directory.CreateDirectory(attachmentPath);
        }

        var uploadResults = new List<TUploadResult>();
        foreach (var file in files)
        {
            uploadResults.Add(await UploadAsync<TUploadResult>(file));
        }

        if (uploadResults.Count > 0)
        {
            //日志记录本次上传最终结果: 任意一个文件上传成功算成功
            var fileNames = uploadResults.Select(x => x.FileName ?? string.Empty).ToString<string>();
            var results = uploadResults.Select(x => x.Result ?? string.Empty).ToString<string>();
            if (uploadResults.Any(x => x.StatusCode == HttpStatusCode.OK))
            {
                _logger.LogInformation($"{_userAccess}: upload the files <{fileNames}> success with result <{results}>!");
            }
            else
            {
                _logger.LogWarning($"{_userAccess}: upload the files <{fileNames}> success with result <{results}>!");
            }
        }

        return uploadResults;
    }

    /// <summary>
    /// 附件的附加操作
    /// </summary>
    /// <param name="contentType">附件文件内容类型</param>
    /// <param name="fileName">文件名</param>
    /// <returns></returns>
    protected virtual async Task AttachAsync(string contentType, string fileName)
    {
        if (_httpSetting.Attachment.Thumbnail.Enable && contentType.StartsWith(AppSettings.CONTENT_TYPE_IMAGE, StringComparison.InvariantCultureIgnoreCase))
        {
            //创建一张默认的图片文件缩略图, 按照 Small 分辨率设置
            var imageQueryOptions = new ImageResizeOptions(Resolution.SM);
            _imageService.Resize(fileName, imageQueryOptions.Width, imageQueryOptions.Height);
        }
        else if (_httpSetting.Attachment.Video.Enable && contentType.StartsWith(AppSettings.CONTENT_TYPE_VIDEO, StringComparison.InvariantCultureIgnoreCase))
        {
            //视频文件抽帧
            GetVideoCaptures(fileName);
        }

        await Task.CompletedTask;
    }
    /// <summary>
    /// 视频附件抽帧
    /// </summary>          
    /// <param name="fileName">视频文件附件文件名</param>
    /// <param name="captureCount">抽帧数量</param>
    /// <returns></returns>
    protected virtual string[]? GetVideoCaptures(string fileName, int captureCount = UNIT_ONE)
    {
        var framesCaptureService = _serviceProvider.GetService<IFramesCaptureService>();
        if (framesCaptureService is null)
        {
            _logger.LogWarning($"{_userAccess}: 尚未注册视频抽帧服务, 无法从视频文件抽取关键帧!");
            return null;
        }

        _logger.LogInformation($"{_userAccess}: 将抽取视频文件: {fileName} {captureCount} 帧!");
        fileName = _fileService.GetAttachmentPath(fileName);
        var results = framesCaptureService.Capture(fileName, captureCount);
        if (results is not null && results.Length > 0)
        {
            _logger.LogInformation($"{_userAccess}: 已抽取视频文件: {fileName} {results.Length} 帧!");
        }
        else
        {
            _logger.LogInformation($"{_userAccess}: 视频文件: {fileName} 抽帧不成功!");
        }

        return results;
    }
    /// <summary>
    /// 图片文件的 resize 处理
    /// 支持的参数形式:
    /// resolution | r = {Resolution}
    /// size | s = {1280x720}
    /// width | w = {int}
    /// height | h = {int}
    /// </summary>
    /// <param name="fileName"></param>
    /// <returns></returns>
    protected virtual string? GetImageFile(string fileName)
    {
        if (_request.QueryString.HasValue)
        {
            _logger.LogInformation($"{_userAccess}: request image resize with query string: {_request.QueryString.Value}");
            var imageQueryOptions = new ImageResizeOptions(_request.QueryString.Value[UNIT_ONE..]);
            var resizedImage = _imageService.GetResizeFileName(fileName, imageQueryOptions.Width, imageQueryOptions.Height);
            if (File.Exists(resizedImage))
            {
                logger.LogInformation($"{_userAccess}: the file {resizedImage} already exists, skipping resize operation.");
                return resizedImage;
            }

            string? fileResult = null;
            resizedImage = _imageService.Resize(fileName, imageQueryOptions.Width, imageQueryOptions.Height);
            if (!string.IsNullOrWhiteSpace(resizedImage) && _httpSetting.Watermark is not null && !string.IsNullOrWhiteSpace(_httpSetting.Watermark.Watermark))
            {
                fileResult = _imageService.AddWatermark(resizedImage);
            }

            if (!(imageQueryOptions.Resolution <= Resolution.Low || imageQueryOptions.Resolution == Resolution.QQVGA || imageQueryOptions.Resolution == Resolution.QVGA) && !string.IsNullOrWhiteSpace(fileResult) && _httpSetting.ImageInformation is not null)
            {
                fileResult = _imageService.AddInformation(fileResult);
            }

            return fileResult;
        }

        return null;
    }
}

/// <summary>
/// 附件及文件操作类服务
/// </summary>
/// <typeparam name="TDbContext">数据库上下文</typeparam>
public class AttachmentService<TDbContext>(
    ILogger<AttachmentService<TDbContext>> logger,
    TDbContext context,
    IDistributedCache cache,
    IHttpContextAccessor httpContextAccessor,
    IFileService fileService,
    IImageService imageService,
    IServiceProvider serviceProvider,
    HttpSetting httpSetting) :
    AttachmentService<TDbContext, Attachment, string>(logger, context, cache, httpContextAccessor, fileService, imageService, serviceProvider, httpSetting),
    IAttachmentService<TDbContext>,
    IDataService<TDbContext, Attachment>
    where TDbContext : DbContext
{
    /// <summary>
    /// 单个文件上传
    /// </summary>
    /// <param name="file">上传的文件</param>
    /// <returns>上传结果</returns>
    public virtual async Task<UploadResult> UploadAsync(IFormFile? file = null) => await base.UploadAsync<UploadResult>(file);
    /// <summary>
    /// 批量文件上传
    /// </summary>
    /// <param name="files">上传的文件</param>
    /// <returns>上传结果</returns>
    public virtual async Task<IEnumerable<UploadResult>> UploadAsync(IEnumerable<IFormFile>? files = null) => await base.UploadAsync<UploadResult>(files);
}